﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using StyleCop;

namespace Jsl.StyleCop.CSharp.UnitTesting
{
    /// <summary>
    /// A helper utility working with analysis results.
    /// </summary>
    [DebuggerStepThrough]
    public static class AnalysisHelper
    {
        private static Dictionary<Assembly, Type[]> assemblyRuleGroupTypes;

        #region Public Properties
        /// <summary>
        /// Gets or sets the method to call when the <see cref="AssertViolations"/> fails.
        /// </summary>
        /// <value>The method to call when the <see cref="AssertViolations"/> fails.</value>
        /// <remarks>
        /// I wanted to be able to raise the failure in this code so I need a delegate to the correct unit testing method
        /// to call. This must be set before the <see cref="Test"/> method is called.
        /// </remarks>
        public static Action<string> AssertFail { get; set; }
        #endregion Public Properties

        #region Public Methods
        /// <summary>
        /// Asserts the violation exists in the list of violations.
        /// </summary>
        /// <param name="actualViolations">The actual violations.</param>
        /// <param name="line">The violation line.</param>
        /// <param name="message">The violation message.</param>
        /// <returns>The remaining violations.</returns>
        public static IList<Violation> AssertViolation(this IList<Violation> actualViolations, int line, string message)
        {
            if (actualViolations == null)
            {
                throw new ArgumentNullException("actualViolations");
            }

            var matchingViolation =
                (from violation in actualViolations
                 where violation.Line == line
                     && violation.Message == message
                 select violation)
                .FirstOrDefault();

            if (matchingViolation == null)
            {
                AssertFail(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "Missing violation on line {0} with message [{1}].",
                        line,
                        message));
            }
            else
            {
                actualViolations.Remove(matchingViolation);
            }

            return actualViolations;
        }

        /// <summary>
        /// Builds the settings file.
        /// </summary>
        /// <param name="ruleType">The type of the rule.</param>
        /// <param name="enabledRuleName">Name of the enabled rule.</param>
        /// <param name="testDirectory">The test directory.</param>
        /// <returns>The file name of the settings file.</returns>
        public static string BuildSettingsFile(
            Type ruleType,
            string enabledRuleName,
            string testDirectory)
        {
            if (ruleType == null)
            {
                throw new ArgumentNullException("ruleType");
            }

            var analyzersElement = new XElement("Analyzers");
            foreach (var ruleGroupType in AnalysisHelper.GetRuleGroupTypes(ruleType.Assembly))
            {
                var analyzerElement =
                    new XElement(
                        "Analyzer",
                        new XAttribute("AnalyzerId", ruleGroupType.FullName));

                analyzersElement.Add(analyzerElement);

                // Build the Rules element with a Rule element for each rule. The enabled rule will be the only one set to true.
                var rulesElement = new XElement("Rules");
                foreach (string ruleName in AnalysisHelper.GetRuleNames(ruleGroupType))
                {
                    var booleanPropertyElement = new XElement(
                        "BooleanProperty",
                        new XAttribute("Name", "Enabled"),
                        ruleName == enabledRuleName ? "True" : "False");

                    rulesElement.Add(
                        new XElement(
                            "Rule",
                            new XAttribute("Name", ruleName),
                            new XElement("RuleSettings", booleanPropertyElement)));
                }

                analyzerElement.Add(rulesElement);
            }

            XDocument settingsDocument = new XDocument(
                new XElement(
                    "StyleCopSettings",
                    new XAttribute("Version", "4.3"),
                    analyzersElement));

            string fileName = Path.Combine(testDirectory, "Settings.StyleCop");
            settingsDocument.Save(fileName);

            return fileName;
        }

        /// <summary>
        /// Asserts that there are no more violations.
        /// </summary>
        /// <param name="actualViolations">The actual violations.</param>
        public static void NoMoreViolations(this IList<Violation> actualViolations)
        {
            if (actualViolations == null)
            {
                throw new ArgumentNullException("actualViolations");
            }

            if (actualViolations.Count > 0)
            {
                var firstViolation = actualViolations[0];
                AssertFail(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "Found unexpected violation on line {0} with message [{1}].",
                        firstViolation.Line,
                        firstViolation.Message));
            }
        }

        /// <summary>
        /// Tests the specified rule.
        /// </summary>
        /// <param name="ruleType">Type of the rule.</param>
        /// <param name="ruleName">Name of the rule.</param>
        /// <param name="sourceCode">The source code.</param>
        /// <returns>The violations from the test.</returns>
        public static IList<Violation> Test(this Type ruleType, string ruleName, string sourceCode)
        {
            // We will use the unit testing shadow directory to put our files in, etc.
            string testDirectory = Path.GetDirectoryName(typeof(AnalysisHelper).Assembly.Location);

            // Create the source code file in the test directory.
            string sourceFileName = Path.Combine(testDirectory, "AnalysisTarget.cs");
            using (var writer = File.CreateText(sourceFileName))
            {
                writer.Write(sourceCode);
            }

            // Build the list add-in paths. This is the directory the test assembly is in.
            List<string> addinPaths = new List<string>();
            addinPaths.Add(testDirectory);

            string settingsFileName = AnalysisHelper.BuildSettingsFile(ruleType, ruleName, testDirectory);

            // Set up the source code analysis for the project.
            var console = new StyleCopConsole(settingsFileName, false, null, addinPaths, false);
            var configuration = new Configuration(new string[] { });
            var project = new CodeProject(sourceFileName.GetHashCode(), sourceFileName, configuration);

            // Add in the single source code file.
            console.Core.Environment.AddSourceCode(project, sourceFileName, null);

            // Build a list of all the violations that are generated.
            var actualViolations = new List<Violation>();
            console.ViolationEncountered += (sender, e) =>
            {
                actualViolations.Add(e.Violation);
            };

            // Process our single source code file.
            CodeProject[] projects = new CodeProject[] { project };
            console.Start(projects, true);

            return actualViolations;
        }
        #endregion Public Methods

        #region Private Methods
        private static IEnumerable<Type> GetRuleGroupTypes(Assembly assembly)
        {
            if (AnalysisHelper.assemblyRuleGroupTypes == null)
            {
                AnalysisHelper.assemblyRuleGroupTypes = new Dictionary<Assembly, Type[]>();
            }

            Type[] ruleGroupTypes = AnalysisHelper.assemblyRuleGroupTypes.TryGetOrCreate<Assembly, Type[]>(
                assembly,
                () =>
                {
                    // Get the rule types in the assembly. If this isn't Jsl.StyleCop.CSharp.dll then add in those types as well.
                    // This will mean they will be weeded out of the analysis when testing is done.
                    IEnumerable<Type> types = assembly.GetTypes();
                    if (assembly != typeof(RefactoringRules.DoNotCompareToBooleanConstant).Assembly)
                    {
                        types = types.Concat(typeof(RefactoringRules.DoNotCompareToBooleanConstant).Assembly.GetTypes());
                    }

                    return
                        (from type in types
                         where type.IsSubclassOf(typeof(SourceAnalyzer))
                         select type).ToArray();
                });

            return ruleGroupTypes;
        }

        private static IEnumerable<string> GetRuleNames(Type analyzerType)
        {
            return
                (from fieldInfo in analyzerType.GetFields(BindingFlags.Static | BindingFlags.Public)
                 where fieldInfo.FieldType == typeof(string)
                 select (string)fieldInfo.GetValue(null)).ToArray();
        }
        #endregion Private Methods
    }
}